// CSP object to prevent global name clobbering
function CSP(){
    // list of events that we can whitelist
    this.allowedEvents = {"onclick":"click", "onload":"load", "ondblclick":"dblclick", 
    		   "onkeydown":"keydown", "onkeypress":"keypress", "onkeyup":"keyup"}

    // nonce size
    this.NONCE_SIZE= 32;

    // nonce
    this.nonce= "";
    // generate a crypto-secure ASCII string
    this.getNonce= function() {
        var buf = new Uint8Array(this.NONCE_SIZE);
        window.crypto.getRandomValues(buf);
        if (!this.nonce) this.nonce = window.btoa(String.fromCharCode.apply(null, buf));
        return this.nonce;
    };

    // Current CSP header
    this.CSP_HEADER= 'X-WebKit-CSP';
    //whitelisted sources
    this.siteWhitelist = ["'self'", "https://ssl.gstatic.com", "http://www.google-analytics.com", "http://www.google.com", "http://ssl.gstatic.com", "https://apis.google.com"]
    // Csp policy, (for experimental purposes I have mixed content in there)
    // Remove the mixed content when deploying
    this.csp_value= {
        "default-src": ["'self'", "chrome-extension:", "'unsafe-inline'"],
        "script-src": ["'unsafe-inline'", "'unsafe-eval'"].concat(this.siteWhitelist),
	"img-src":["*"],
        "script-nonce": [this.getNonce()],
        "frame-src": ["about:", "https:"]
    };


    // if site authors want to whitelist certain inline events
    this.tagWhitelist = {};
    this.handlerWhitelist = {};
    this.allowJsUrl = true; //always true for now

    // allows site authors to specify the types of inline handlers to whitelist
    this.addWhitelistHandler = function(handler){
	//checks if handler is valid
    	if ( !(handler in this.handlerWhitelist) &&
		(handler in this.allowedEvents)){
	   this.handlerWhitelist[handler] = this.allowedEvents[handler]
	}
    }

    // allows site authors to specify which tag to allow inline handlers
    // must be a valid html tag
    this.addWhitelistTag= function(tag) {
	if (! (tag in this.tagWhitelist)){
	   this.tagWhitelist[tag] = 1;
	}
    }

    // construct CSP directories
    this.objectToCSP= function(obj) {
        var dir = [];
        for (var n in obj) {
            dir.push([n].concat(obj[n]).join(' '));
        }
        return dir.join(";");
    };


    // Inline event handler translator.
    // we want to find all inline handlers and turn them into external
    // Parameters: 
    //     tag - HTML tag name to whitelist
    //     handler - the inline handler to whitelist
    this.eventTranslation = function(tag, handler) {
        var body = document.getElementsByTagName("body")[0] || document.documentElement;
	var tags = document.getElementsByTagName(tag);
	for (var i=0; i<tags.length; i++){
	   // we want to look over all the tags and move inline to external
	   var inlineString = tags[i].getAttribute(handler);
           if (inlineString){
	      //console.log(tag+" tag detected with value: "+inlineString);
	      tags[i].addEventListener(this.handlerWhitelist[handler], 
			// closure induced funcky function call
			(function(staticVal){
	                        // we use eval here because we trust the onclick attribute
				return function(){eval(staticVal)}
			})(inlineString), 
			false);
	      tags[i].removeAttribute(handler,0);// remove the inline handler
	   } 
	}
    }

    // JavaScript URL translator.
    // find all JS Urls and turn them into real scripts. Currently, this function only 
    // looks at <a> tags because JS Url will only work for <a> and <iframe>: 
    // http://www.w3.org/TR/2011/WD-html5-20110525/webappapis.html#javascript-protocol
    this.jsUrlTranslation = function(){
        var body = document.getElementsByTagName("body")[0] || document.documentElement;
	var tags = document.getElementsByTagName("a");
	for (var i=0; i<tags.length; i++){
	   // if it is a javascript url
	   if (tags[i].protocol == "javascript:"){
	   	var scriptValue = tags[i].href.substring(11);
		// change it into an external onclick event via eval
		tags[i].addEventListener("click",
			// closure induced funcky function call
			(function(staticVal){
				return function(){eval(staticVal)}
			})(scriptValue),
			false);
		tags[i].removeAttribute("href",0);
	   }
	}
    }

    // checks if the url string has a whitelisted origin
    this.isUrlWhitelisted = function(url){
	// hacky way of using the brower's URL parser to parse for us
    	var urlParser = document.createElement("a");
	urlParser.href = url;
	var origin = urlParser.origin;

	if (origin){
	   var whitelistParser = document.createElement("a");
	   //check the origin against our whitelist
	   for (var i=0; i<this.siteWhitelist.length;i++){
		whitelistParser.href = this.siteWhitelist[i];
		if (this.siteWhitelist[i] == 'self' &&
		    document.location.origin == origin){
		   return true;
		}else if (whitelistParser.origin == origin){
		   return true;
		}else return false;
	   }

	}else{
	   // the string is not a valid URL
	   return false;
	}
    }

    // Main function that gets called once
    // This function does the following:
    // 1. hooks the appendChild, insertBefore, and replaceChild method
    //    to whitelist dynamically loaded scripts. The script must be either 
    //    inline or comes from a whitelisted source (to prevent src injection).
    // 2. After the initial page is loaded. It crawls the page for any inline handlers
    //    and turn these handlers into external handlers by using eval.
    this.run= function() {
	// "that" points to the CSP object
	var that = this;

	// appendChild hook to catch all dynamically injected script nodes
	Node.prototype.appendChild = function(input){
	    return function() {
		var ele = arguments[0];
		if (ele.tagName.toLowerCase() === "script") {
	           //console.log("appendChild hook called with src: "+ele.getAttribute("src"));
	           // checks for src injection
		   if (!ele.getAttribute("src") ||
			that.isUrlWhitelisted(ele.getAttribute("src"))){
			ele.setAttribute("nonce", that.getNonce());
		   }
		}
		//call the original function with the modified element
		var script_ = input.apply(this, arguments);
		return script_;
	    };
	}(Node.prototype.appendChild)

	// replaceChild hook to catch all dynamically injected script nodes
	Node.prototype.replaceChild = function(input){
	    return function() {
		var ele = arguments[0];
		if (ele.tagName.toLowerCase() === "script") {
	           //console.log("appendChild hook called with src: "+ele.getAttribute("src"));
	           //check for src injection
		   if (!ele.getAttribute("src") ||
			that.isUrlWhitelisted(ele.getAttribute("src"))){
			ele.setAttribute("nonce", that.getNonce());
		   }
		}
		//call the original function with the modified element
		var script_ = input.apply(this, arguments);
		return script_;
	    };
	}(Node.prototype.replaceChild)

	// insertBefore hook to catch all dynamically injected script nodes
	Node.prototype.insertBefore = function(input){
	    return function() {
		var ele = arguments[0];
		if (ele.tagName.toLowerCase() === "script") {
	           //console.log("appendChild hook called with src: "+ele.getAttribute("src"));
	           //check for src injection
		   if (!ele.getAttribute("src") ||
			that.isUrlWhitelisted(ele.getAttribute("src"))){
			ele.setAttribute("nonce", that.getNonce());
		   }
		}
		//call the original function with the modified element
		var script_ = input.apply(this, arguments);
		return script_;
	    };
	}(Node.prototype.insertBefore)


	// this function is called when all CSS JS are loaded
        document.addEventListener("DOMContentLoaded", function() {
	    //console.log(document.documentElement.innerHTML);

	    // translate all javascript URLs to external
	    if (that.allowJsUrl){
	    	that.jsUrlTranslation();
	    }

	    // translate all inline handlers to external
	    for(var tag in that.tagWhitelist){
	    	for (var handler in that.handlerWhitelist){
		    //console.log("Turning '"+tag+"' '"+handler+"' into external");
            	    that.eventTranslation(tag, handler);
		}
	    }
			
            // inject CSP meta header at the end of script load
	    var head = document.getElementsByTagName("head")[0] || document.documentElement;
            var meta = document.createElement("meta");
            meta.setAttribute('http-equiv', that.CSP_HEADER);
            var cspDir = that.objectToCSP(that.csp_value);
            //console.log(cspDir);
            meta.setAttribute('content', cspDir);
            head.appendChild(meta);
        }, false);
    }
}

var dominatrixss = new CSP();

